"""

.. module:: htmltemplate
.. moduleauthor:: @author: 

This module generates content from a template and a resource.

class YarraBase(object) provides a method to create the correct URI.

class YarraList(YarraBase) builds the branches from the yarra:loop tags during 
parsing. in the generation phase, it processes each branch by recursively 
calling generate.

class YarraCond(YarraList) build a conditional branch during parsing and 
includes or omits the section during generation based on a comparison with test.

class YarraData(YarraBase) captures non-yarra text during parsing and adds this 
to the content during generation.

class YarraVar(YarraBase) records variables during parsing and performs variable 
substitution during generation.

class YarraHTMLParser(HTMLParser) Parses the template and generates a tree from
the yarra prefixed tags.

class HTMLTemplate(object) defines the API for this module.
   
.. seealso:: modules :mod:`<module>`

*Constants*:
.. data:: constant_1
        
    constant_1 is used for ......
        
.. data:: constant_2
        
    constant_2 is used for ......
"""

# Imports
#
import sys
import types
import re
from Queue import Queue
from HTMLParser import HTMLParser

from datastore.resource import Resource, ResourceDict
from runtime.spaces import Spaces, SpacesContext

# Constants
#

# Classes
#

class YarraBase(object):
    
    def build_uri(self, base, args, named_uris):
        """
        base is the current URI. May be None.
        args is a dict containing:
            uri = Full uri or fragment.
            var = Part of the URI passed as an argument.
            rel = Append to current URI.
            name = save the current uri+rel+ref in an array as this text.
            ref = use the saved uri instead of URI.
            indirect = take the result as a URI and return its contents.
            
        named_uris is a dict with the current named uri's. Added to as needed.
        
        Only one of ref, uri or var can be specified (tested in the order 
        shown).
         
        Returns a URI string.
        """
        #print(named_uris)
        uri = base
        if args.get('ref') is not None:
            ref = args.get('ref')
            if ref in named_uris:
                uri = named_uris[ref]
        if args.get('uri') is not None:
            uri = args.get('uri')
        if args.get('var') is not None:
            uri = '#' + args.get('var')
        if args.get('rel') is not None:
            if uri is None:
                uri = args.get('rel')
            else:
                uri += "." + args.get('rel')
        
        #print("[build_uri] uri=%s base=%s args=%s named_uris=%s" % \
        #    (uri, base, args, named_uris))
        return uri

    def get_res_data(self, uri, res, spaces_context, indirect=None):
        if str(indirect).lower() == "true":
            uri = self.get_res_data(uri, res, spaces_context, indirect=False)
        if uri.startswith('#'):
            data = res.get_var_part(uri[1:])
        else:
            data = spaces_context.get_res_part(spaces_context.uri(uri))
        return data
        
    def uri_set_name(self, uri, args, named_uris):
        if args.get('name') is not None:
            named_uris[args.get('name')] = uri  
        
class YarraList(YarraBase):
    """
    Internal template class to handle a template with the tag 
    <yarra:loop ... >.
    """
    
    def __init__(self, args, spaces_context, verbose=False):
        """ Build the parse tree by creating a new list."""
        self.args = args
        self.spaces_context = spaces_context
        self.branches = []
        self.verbose = verbose

    def add_list(self, args, verbose=False):
        """
        Build the parse tree by adding a new list.
        Used by YarraList and YarraCond.
        """
        n = YarraList(args, self.spaces_context, verbose=verbose)
        self.branches.append(n)
        return n

    def add_cond(self, args, verbose=False):
        """ 
        Build the parse tree by adding a new conditional section.
        Used by YarraList and YarraCond.
        """
        n = YarraCond(args, self.spaces_context, verbose=verbose)
        self.branches.append(n)
        return n

    def add_data(self, data, verbose=False):
        """ 
        Build the parse tree by adding new data.
        Used by YarraList and YarraCond.
        """
        d = YarraData(data, self.spaces_context, verbose=verbose)
        self.branches.append(d)
        return d

    def add_var(self, args, verbose=False):
        """ 
        Build the parse tree by adding a new variable.
        Used by YarraList and YarraCond.
        """
        v = YarraVar(args, self.spaces_context, verbose=verbose)
        self.branches.append(v)
        return v
    
    def generate(self, res, base, var_key, named_uris):
        """
        YarraList uses the parse tree and a resource to generate output. Access 
        to other resources in Spaces are possible by using fully qualified URIs.
        
        The first time generate is called, base will be None.
        
        var_key is the name of the current loop variable (key or count).
        """
        content = ''        
        uri = self.build_uri(base, self.args, named_uris)
        if uri is None:
            list = None
        else:
            try:
                list = self.get_res_data(uri, res, self.spaces_context, 
                                         indirect=self.args.get('indirect'))
            except:
                content = "<!-- exception %s %s -->" % \
                        ('', str(sys.exc_info()[1]))
                list = None
        #print("YarraList uri=%s args=%s base=%s named_uris=%s" % \
        #    (uri, self.args, base, named_uris))

        if type(list) in [types.DictType, type(ResourceDict())]:
            iter = list.keys()
            if iter is not None:
                iter.sort()
            for key in iter:
                if len(uri) == 0:
                    base = str(key)
                else:
                    base = uri + "." + str(key)
                self.uri_set_name(base, self.args, named_uris)
                for b in self.branches:
                    content += b.generate(res, base, key, named_uris)
        elif type(list) in [types.ListType, types.TupleType]:
            count = 0
            for element in list:
                if len(uri) == 0:
                    base = str(count)
                else:
                    base = uri + "." + str(count)
                self.uri_set_name(base, self.args, named_uris)
                for b in self.branches:
                    content += b.generate(res, base, count, named_uris)
                count += 1
        else:
            # does nothing useful (should this be an error?)
            for b in self.branches:
                content += b.generate(res, uri, None, named_uris)
            pass
        #
        return content
    
    def dump(self):
        """ Diagnostic function."""
        print("YarraList/Cond dump %s" % (self.args))
        for b in self.branches:
            b.dump()

class YarraCond(YarraList):
    """
    Internal template class to handle a template with the tag 
    <yarra:cond ... >.
    """
    
    def __init__(self, args, spaces_context, verbose=False):
        """ Build the parse tree by creating a new conditional section."""
        self.args = args
        self.spaces_context = spaces_context
        self.branches = []
        self.verbose = verbose
        if args.get('test') is not None:
            self.test = args.get('test')
        else:
            self.test = None
        if args.get('indirect') is not None:
            self.indirect = args.get('indirect')
        else:
            self.indirect = 0
    
    def generate(self, res, base, var_key, named_uris):
        """
        YarraCond uses the parse tree and a resource to generate output. Access 
        to other resources in Spaces are possible by using fully qualified URIs.
        
        The first time generate is called, base will be None.
        """
        content = ''        
        uri = self.build_uri(base, self.args, named_uris)
        if uri is None:
            value = None
        else:
            try:
                value = self.get_res_data(uri, res, self.spaces_context, 
                                          indirect=self.args.get('indirect'))
            except:
                content = "<!-- exception %s %s -->" % \
                        ('', str(sys.exc_info()[1]))
                value = None
            
        if 'test' in self.args:
            test = self.args.get('test')

            # TODO: What should happen if 'value' is equal to the string "None"?
            #
            if test == str(value):
                self.uri_set_name(uri, self.args, named_uris)
                for b in self.branches:
                    content += b.generate(res, uri, None, named_uris)
        elif 'ntest' in self.args:
            test = self.args.get('ntest')
            
            # TODO: What should happen if 'value' is equal to the string "None"?
            #
            if test <> str(value):
                self.uri_set_name(uri, self.args, named_uris)
                for b in self.branches:
                    content += b.generate(res, uri, None, named_uris)
        else:
            content += \
            "<!-- Warning: test or ntest must be specified for yarra:cond -->"
        #
        return content

class YarraVar(YarraBase):
    """
    Internal template class to handle a template with the tag 
    <yarra:var ... > and to perform a variable substitution in the
    generated output.
    """

    def __init__(self, args, spaces_context, verbose=False):
        self.args = args
        self.spaces_context = spaces_context
        self.verbose = verbose
        self.value = None

    def generate(self, res, base, var_key, named_uris):
        return self.generate_var(res, base, var_key, named_uris)
        
    def generate_var(self, res, base, var_key, named_uris):
        """
        """
        uri = self.build_uri(base, self.args, named_uris)
        self.uri_set_name(uri, self.args, named_uris)
        try:
            self.value = self.get_res_data(uri, res, self.spaces_context, 
                                           indirect=self.args.get('indirect'))
        except:
            if self.verbose:
                self.value = "<!-- exception %s %s -->" % \
                    ('', str(sys.exc_info()[1]))
        #print("YarraVar u=%s args=%s val=%s var=%s uris=%s" % \
        #      (uri, self.args, self.value, var_key, named_uris))
        
        if str(self.args.get('key')).lower() == "true":
            self.value = var_key
            
        if self.value is None:
            return "<!-- %s=None -->" % uri
        else:
            return str(self.value)

    def dump(self):
        print("YarraVar %s %s" % (self.value, self.args))

class YarraData(YarraVar):
    """
    Internal template class to handle data in the template and to add data to
    the generated output.
    """
    
    def __init__(self, data, spaces_context, verbose=False):
        self.data = data
        self.spaces_context = spaces_context
        self.verbose = verbose
        self.args = None
        self.value = None

    def generate(self, res, base, var_key, named_uris):
        #print("YarraData {{{%s}}}" % self.data)
        
        # process the variable
        def sub_var(m):
            attrs = {}
            for attr in re.findall("""\s*(.*?)=['"](.*?)['"]\s*""", 
                                   m.groups()[1]):
                attrs[attr[0]] = attr[1]
            self.args = attrs
            return  m.groups()[0] + \
                    self.generate_var(res, base, var_key, named_uris) + \
                    m.groups()[2]

        # search for any variables within the data and process any found.
        self.value = re.sub("(.*?)\{yarra:var(.*?)\}(.*?)", sub_var, self.data)
        return self.value

    def dump(self):
        print("YarraData {{{%s}}}" % self.data)
    
class YarraHTMLParser(HTMLParser):
    """
    Internal class used by HTMLTemplate.
    
    Parse the text and use the yarra:loop, yarra:cond and yarra:var tags to 
    construct a tree made up of YarraList, YarraCond, YarraData and YarraVar 
    objects.
    
    Tag:
        <yarra:loop ...>
            uri = Full uri usually including a fragment.
            var = a variable from the Resource passed to the template.
            rel = Append to current URI.
            name = save the current uri+rel+ref in an array as this text.
            ref = use the saved uri instead of URI.
            comment = text to add to generated HTML if verbose=True.
        <yarra:cond ...>
            uri
            var
            rel
            name
            ref
            comment
            indirect = 0 is none, 1 is once, etc.
            test = string to compare against the value specified by uri/rel.
            ntest = not test.
        <yarra:var ...> OR {yarra:var ...}
            uri
            var
            rel
            name
            ref
            key=True|False use the key/count rather than the value
            comment
            indirect
    
    If uri and ref are omitted, then the current loop value is used.
    If the URI resolves to a resource, then its variables are iterated over.
    
    Note: 'comment' is processed during parsing and and 'yarra:include' 
    is processed in a separate phase.
    """

    def __init__(self, spaces_context, template_content, verbose=False):
        HTMLParser.__init__(self)
        self.base = YarraList({}, spaces_context, verbose=verbose)
        # The stack contains nested YarraList and the other objects. 
        # The base list name is None.
        self.spaces_context = spaces_context
        # This is included from within the site template.
        self.template_content = template_content
        self.yarra_stack = [self.base]
        self.verbose = verbose
        
    def handle_starttag(self, tag, attrs):
        #print "Encountered the beginning of a %s tag" % tag
        attr_hash = dict(attrs)
        if tag.startswith("yarra:"):
            if self.verbose:
                comment = "<!-- %s %s pos%s -->" % \
                    (tag, attr_hash, self.getpos())
                self.yarra_stack[-1].add_data(comment, verbose=self.verbose)
            if 'comment' in attr_hash.keys():
                comment = "<!-- %s -->" % attr_hash['comment']
                self.yarra_stack[-1].add_data(comment, verbose=self.verbose)
        if tag == 'yarra:loop':
            e = self.yarra_stack[-1].add_list(attr_hash, verbose=self.verbose)
            self.yarra_stack.append(e)
        elif tag == 'yarra:cond':
            e = self.yarra_stack[-1].add_cond(attr_hash, verbose=self.verbose)
            self.yarra_stack.append(e)
        elif tag == 'yarra:var':
            self.yarra_stack[-1].add_var(attr_hash, verbose=self.verbose)
        else:
            d = self.make_tag(tag, attrs)
            self.yarra_stack[-1].add_data(d, verbose=self.verbose)

    def handle_endtag(self, tag):
        #print "Encountered the end of a %s tag" % tag
        if tag in ['yarra:loop', 'yarra:cond']:
            if self.verbose:
                comment = "<!-- /%s -->" % tag
                self.yarra_stack[-1].add_data(comment, verbose=self.verbose)
            self.yarra_stack.pop()
        elif tag in ['yarra:var']:
            pass
        else:
            d = "</%s>" % tag
            self.yarra_stack[-1].add_data(d, verbose=self.verbose)
        
    def handle_data(self, data):
        #print "Encountered data: %s" % data
        self.yarra_stack[-1].add_data(data, verbose=self.verbose)
            
    def handle_comment(self, data):
        #print "Encountered comment: %s" % data
        comment = "<!--%s-->" % data
        self.yarra_stack[-1].add_data(comment, verbose=self.verbose)

    def handle_charref(self, data):
        #print "Encountered charref: %s" % data
        char = "&#%s;" % data
        self.yarra_stack[-1].add_data(char, verbose=self.verbose)
        
    def handle_entityref(self, data):
        #print "Encountered entityref: %s" % data
        entity = "&%s;" % data
        self.yarra_stack[-1].add_data(entity, verbose=self.verbose)
    
    def handle_decl(self, data):
        """Ref: http://htmlhelp.com/tools/validator/doctype.html"""
        #print "Encountered decl: %s" % data
        decl = "<!%s>" % data
        self.yarra_stack[-1].add_data(decl, verbose=self.verbose)
    
    def handle_pi(self, data):
        #print "Encountered processing instruction: %s" % data
        pi = "<?%s>" % data
        self.yarra_stack[-1].add_data(pi, verbose=self.verbose)
    
    def make_tag(self, tag, attrs):
        a = ''
        for k, v in attrs:
            a += ' %s="%s"' % (k, v)
        return "<%s%s>" % (tag, a)
    
    def dump(self):
        self.base.dump()
        
    def finish(self):
        #print("YarraHTMLParser:finish %s" % self.base.dump())
        return self.base

    
class YarraIncludeParser(HTMLParser):
    """
    Internal class used by HTMLTemplate.
    
    Parse yarra:include statements. Leave everything else alone.
    
    Tag:
        yarra:include
            uri
            comment
    
    If uri is omitted, then the template is included.
    
    The site_template is passed using parse.feed(). It must contain at least one
    include (without a URI) for the actual template.
    """

    def __init__(self, spaces_context, template_content, verbose=False):
        HTMLParser.__init__(self)
        self.spaces_context = spaces_context
        self.template_content = template_content
        self.verbose = verbose
        self.content = ''
        self.substitute = False
        
    def handle_starttag(self, tag, attrs):
        attr_hash = dict(attrs)
        if tag == 'yarra:include':
            self.substitute = True
            if self.verbose:
                self.content += "<!-- %s %s pos%s -->" % \
                    (tag, attr_hash, self.getpos())
            if 'comment' in attr_hash.keys():
                self.content += "<!-- %s -->" % attr_hash['comment']
            if ('uri' in attr_hash) and (attr_hash['uri'] is not None):
                # We have a URI to include. It will be a reference to a
                # Resource, in which case we will include res.content, otherwise
                # its a reference to the actual content, which we will include.
                uri = attr_hash['uri']
                something = self.spaces_context.get_res_part(
                                self.spaces_context.uri(uri))
                if type(something) in [type(Resource()), type(ResourceDict())]:
                    if something.content is None:
                        self.content += \
                            "<!-- Warning: No template content in %s" % uri
                    else:
                        self.content += something.content
                elif type(something) is types.NoneType:
                    self.content += \
                        "<!-- Warning: uri (%s) returned None. -->" % \
                        uri
                else:
                    self.content += something
            else:
                # if a URI is not specified, then assume that we want to
                # insert our template here.
                self.content += self.template_content
        else:
            self.content += self.make_tag(tag, attrs)

    def handle_endtag(self, tag):
        if tag <> 'yarra:include':
            self.content += "</%s>" % tag
        
    def handle_data(self, data):
        self.content += data
            
    def handle_comment(self, data):
        self.content += "<!--%s-->" % data

    def handle_charref(self, data):
        self.content += "&#%s;" % data
        
    def handle_entityref(self, data):
        self.content += "&%s;" % data
    
    def handle_decl(self, data):
        self.content += "<!%s>" % data
    
    def handle_pi(self, data):
        self.content += "<?%s>" % data
    
    def make_tag(self, tag, attrs, single=False):
        a = ''
        for k, v in attrs:
            a += ' %s="%s"' % (k, v)
        if single:
            return "<%s%s />" % (tag, a)
        else:
            return "<%s%s>" % (tag, a)

class HTMLTemplate(object):
    """
    Class to define a clean API for text (usually XML or HTML) generation from 
    a template and a resource.

    The text must contain the appropriate tags and must be parseable as HTML.
    
    site_template may be None. If provided, it must contain an empty include:
        <yarra:include />
    
    usage:
        x = HTMLTemplate(spaces_context, site_template, template, verbose=True)
        x.generate(res)
    """
    MAX_TEMPLATE_RECURSION = 10
    
    def __init__(self, spaces_context, site_template_res, 
                 template_res, data_res, verbose=False):
        self.spaces_context = spaces_context
        self.site_template_res = template_res
        self.template_res = template_res
        self.data_res = data_res
        self.verbose = verbose
        self.parser = None

        # We start with a site template (if one is provided)
        if site_template_res is None:
            content = """<yarra:include />"""
        else:
            content = site_template_res.content
        
        # Recursively parse first pass - process includes
        substitute = True
        recursion_level = 0
        while substitute:
            parser = YarraIncludeParser(self.spaces_context,
                                 self.template_res.content,
                                 self.verbose)
            parser.feed(content)
            substitute = parser.substitute
            content = parser.content
            recursion_level += 1
            if recursion_level > self.MAX_TEMPLATE_RECURSION:
                substitute = False
                content += \
                    "<!-- Error: Maximum recursion level (%s) exceeded. -->" % \
                    self.MAX_TEMPLATE_RECURSION
        expanded_template = content

        # Parse second pass - process the template content.
        self.parser = YarraHTMLParser(self.spaces_context, 
                                 self.template_res, self.verbose)
        # specify the site template (which must include the actual template).
        self.parser.feed(expanded_template)
    
    def dump(self):
        print("HTMLTemplate dump parsed HTML.")
        self.parser.dump()
        
    def generate(self):
        # Start generation with the resource, no base URI and no named URI's.
        # the base uri is specified as none so we don't initially iterate over 
        # the resource variables.
        return self.parser.finish().generate(self.data_res, None, None, {})
    